--random values for generate amounts
function lowGenerate()
	return tonumber(GetConVarNumber("maddogs_resource_systems_lowGenerate") or 500)
end
function highGenerate()
	return tonumber(GetConVarNumber("maddogs_resource_systems_highGenerate") or 500)
end
function lowConsume()
	return tonumber(GetConVarNumber("maddogs_resource_systems_lowConsume") or 500)
end
function highConsume()
	return tonumber(GetConVarNumber("maddogs_resource_systems_highConsume") or 500)
end

LIFE = function( self )
	return VOLUME(self) * math.random(0.9, 1.1)
end

VOLUME = function( self )
	return self._rs.volume / 3
end

GENERATE = function( self )
	local resources = RS.GetTable(self, self._rs.requires) or {}
	local requires = table.Count(resources)

	if (requires == 0) then requires = 1 end

	--extra boost for harder to get resources
	if (table.Count(resources) >= 1) then
		if (resources.Petrol) then requires = requires + 3 end
		if (resources.Oil) then requires = requires + 2 end
		if (resources.Hydrogen) then requires = requires + 1 end
		if (resources.Water) then requires = requires + 1 end
	end

	return ((VOLUME(self) / math.random(lowGenerate(), highGenerate())) * requires) * (RS.GetHealth(self)/100)
end

STORAGE = function( self )
	return VOLUME(self) * (RS.GetHealth(self)/100)
end

CONSUME = function( self )
	return (VOLUME(self) / math.random(lowConsume(), highConsume())) * (RS.GetHealth(self)/100)
end

RADIUS = function( self )
	return self:GetDistanceAmount() * (RS.GetHealth(self)/100)
end

INC = function( self )
	return (RS.GetHealth(self)/100)
end

TAKEDAMAGE = function( self, dmg )
	--self.BaseClass.OnTakeDamage( self, dmg )

	if (RS.GetHealth(self) == 0) then return end --dead

	local stored = self:NodeStoredAmount()

	local _stored = RS.GetTable(self, self._rs.stored)
	local _resources = RS.GetTable(self, self._rs.resources)
	local water = nil
	local energy = nil

	if (_stored.Energy) then energy = _stored.Energy end
	if (_stored.Water) then water = _stored.Water end

	--do energy damage
	if (energy && _stored.Energy > 0) then
		self:EnergyDamage( dmg:GetDamagePosition(), dmg:GetDamage())
	elseif (water && _stored.Water > 0) then
		self:CreateWaterEffect( dmg:GetDamagePosition() )
	end

	if (_resources && _resources.Energy) then --if it provides energy then make the device smoke
		self:MakeSmoke()
	end

	--leak all stored resources in this device
	for _, name in pairs(_stored or {}) do
		self:CommitResources( {[name] = -math.random(10, 100)}, true )
	end

	--reduce health
	RS.SetHealthInc( self, (dmg:GetDamage() / 5) )

	--should we fail and turn off?
	if (math.random(1, 10) < 8) then self:TurnOff() end



	local ed = EffectData()
	ed:SetEntity( self )
	util.Effect( "sc_shield_hit", ed, true, true)
end


function RS.LSGive(ent, name, amount)
	if (ent.LEnvironment && ent.LEnvironment.OnPlanet) then
		local p = ent.LEnvironment.OnPlanet

		if (p) then
			p = MDSB.Environments.Entities[p]

			if (p && name == "Air" && type(p.Environment) != "function" && type(p.Environment.oxygen) != "function") then
				p.Environment.oxygen = RS.GetNumber(p, p.Environment.oxygen + (amount / 100000))

				MsgN("Restore Oxygen for Storage Leak: " .. p.Environment.oxygen)
			end
		end
	end
end

function RS.LSSteal(ent, name, amount)
	if (ent.LEnvironment && ent.LEnvironment.OnPlanet) then
		local p = ent.LEnvironment.OnPlanet

		if (p) then
			p = MDSB.Environments.Entities[p]

			if (p && name == "Air" && type(p.Environment) != "function" && type(p.Environment.oxygen) != "function") then
				p.Environment.oxygen = RS.GetNumber(p, p.Environment.oxygen - (amount / 100000))

				MsgN("Take Oxygen for Storage: " .. RS.GetNumber(p, p.Environment.oxygen))
			end
		end
	end
end



--set prop owner
function RS.PlayerSpawnedProp( ply, model, ent )
	ent:SetOwner( ply )
	ent.Owner = ply
end
hook.Add( "PlayerSpawnedProp", "RS.PlayerSpawnedProp", RS.PlayerSpawnedProp )

--adds LS info
RS.Setup = function( ent, options )
	if (!ent) or (!ent:IsValid()) then return end

	local phys = ent:GetPhysicsObject()
	local MaxVolume = 10

	if (phys:IsValid()) then
		phys:Wake()
		phys:SetMass(math.ceil(phys:GetVolume() or 100 / 250))		--set mass based off volume
		MaxVolume = math.ceil(phys:GetVolume() or 100 / 10)			--save max volume
	end

	if (!ent._rs) then
		ent._rs = {}
		ent._rs.name = ""
		ent._rs.desc = ""
		ent._rs.title = ""
		ent._rs.volume = MaxVolume
		ent._rs.active = 0
	end
	--[[
		entities
		produces
		stored
		storage
	]]

	function ent:Node( node )
		return RS.Node( ent, node )
	end

	function ent:Update()
		return RS.Update( ent )
	end

	function ent:NodeProducesAmount()
		--return the totals
		return self:Node()._produces
	end

	function ent:NodeStoredAmount()
		--return the totals
		return self:Node()._stored
	end

	function ent:NodeStorageAmount()
		--return the totals
		return self:Node()._storage
	end

	function ent:GetGenerateAmount()
		return GENERATE( ent )
	end

	function ent:GetDistanceAmount()
		return GENERATE( ent )
	end

	function ent:GetStorageAmount()
		return STORAGE( ent )
	end

	function ent:GetConsumeAmount()
		return CONSUME( ent )
	end

	function ent:GetVolume()
		return VOLUME( ent )
	end

	function ent:GetRequirements()
		local requires = {}

		--check requirements the entity may have
		for name, value in pairs( self._rs.requires or {} ) do
			--make sure we have a number
			local value2 = RS.GetNumber(self, value)

			requires[name] = -value2
		end

		return requires
	end

	function ent:CommitResources( resources, justdoit )
		return RS.CommitResources( ent, resources, justdoit )
	end

	function ent:Update()
		return RS.Update( ent )
	end

	function ent:IsActive()
		return (ent._rs.active == true)
	end

	function ent:SetActive()
		ent._rs.active = true

		--update Wiremod
		ent:WireUpdate( "Active", 1 )
	end

	function ent:SetInActive()
		ent._rs.active = false

		--update Wiremod
		ent:WireUpdate( "Active", 0 )
	end

	--apply the custom settings
	for name, value in pairs( (options or {}) ) do
		--save setting to entity
		ent._rs[name] = value
	end

	--if set as storage make sure the stored amount is set to zero
	for name, value in pairs( RS.GetTable(ent, ent._rs.storage) or {} ) do
		if (!ent._rs.stored) then ent._rs.stored = {} end

		--save setting to entity
		if (type(ent._rs.stored) != "function" && !ent._rs.stored[name]) then ent._rs.stored[name] = 0 end
	end

	if (options) then
		for name, value in pairs( (options.BaseClass or {}) ) do
			--save setting to entity
			ent[name] = value
		end
	end

	if (ent._rs.status) then ent:WireInput( "Active" ) end

	--update system faster once entity is removed
	ent:CallOnRemove("RSRemove", function()
		timer.Simple(0.01, function()
			RS.Update( ent )
		end)
	end)

	--destory timer just incase the entity already has a timer
	timer.Destroy("Update" .. ent:EntIndex())

	--create the think timer that will do stuff
	timer.Create("Update" .. ent:EntIndex(), RS.NodeUpdateTime(), 0, function(self)
		if ValidEntity(self) then
			self:Update()
		else
			timer.Destroy("Update" .. ent:EntIndex())
		end
	end, ent)

	ent:SetMaxHealth(100)
	ent:SetHealth(100) --new device, new health

	function ent:Repair()
		ent:SetHealth(100) --repair

		ent:SetColor( 255, 255, 255, 255 )
	end

	--force update right now
	RS.Update( ent )
end

RS.SetHealth = function( self, health )
	self:SetHealth( health )
end

RS.GetHealth = function( self )
	return self:Health()
end

RS.SetHealthInc = function( self, inc )
	self:SetHealth( RS.GetHealth(self) - inc )

	if (RS.GetHealth(self) < 0) then RS.SetHealth(self, 0) end --its dead
end

--gets or sets the RS for the entity
RS.Node = function( self, node )
	--make sure valid
	if (!self) or (!self:IsValid()) or (!beams) then return end

	--if passed in a new node entity, save it
	if ValidEntity(node) then self._rs.entity = node end

	--if the RS isnt valid, set entity as RS
	if (!ValidEntity(self._rs.entity)) then self._rs.entity = (node or self) end

	--get the node resource information
	local node = self._rs.entity._rs

	--timer check to make sure node isnt updated to often
	if (CurTime() >= (node.update or -10)) then
		--dont update for 1 second
		node.update = CurTime() + RS.NodeUpdateTime()

		--START: RECALCULATE NODE PRODUCES, STORED AND STORAGE VALUES
		--update values
		node.entities, node._produces, node._stored, node._storage = RS.GetConnected(self), {}, {}, {}

		--loop through the entities
		for _, ent in pairs( node.entities ) do
			if (!ent or !ValidEntity(ent) or !ent._rs) then continue end --next please

			local resources = RS.GetTable(ent, ent._rs.resources)
			local stored = RS.GetTable(ent, ent._rs.stored)
			local storage = RS.GetTable(ent, ent._rs.storage)

			--produce and/or consume values
			for name, value in pairs( resources or RS.Resources ) do
				if (!table.HasValue(RS.Resources, name)) then table.insert(RS.Resources, name) end
				node._produces[name] = RS.GetNumber(ent, value, node._produces[name])
			end

			--max storage values
			for name, value in pairs( storage or RS.Resources ) do
				if (!table.HasValue(RS.Resources, name)) then table.insert(RS.Resources, name) end
				node._storage[name] = RS.GetNumber(ent, value, node._storage[name])
			end

			--stored values
			for name, value in pairs( stored or {} ) do
				if (!table.HasValue(RS.Resources, name)) then table.insert(RS.Resources, name) end
				node._stored[name] = RS.GetNumber(ent, value, node._stored[name])
			end
		end

		--loop through the entities to update wire
		for _, ent in pairs( node.entities ) do
			if (!ent or !ValidEntity(ent) or !ent._rs) then continue end --next please

			local stored = RS.GetTable(ent, ent._rs.stored)

			--stored values
			for name, value in pairs( stored or {} ) do
				--update Wiremod outputs
				if (ent._rs.stored && RS.GetTable(ent, ent._rs.stored)[name]) then
					ent:WireUpdate( name, node._stored[name] )
					ent:WireUpdate( "Max: " .. name, node._storage[name] )
				end
			end
		end
		-- END: RECALCULATE NODE PRODUCES, STORED AND STORAGE VALUES
	end

	--return the Resource System setup from the Node Entity
	return node
end

--returns all entities that are connected to a single entity that is passed in
RS.GetConnected = function( self )
	if (!beams) then return {} end
	return beams.Connected(self, true)
end

--the Think function to provide and/or consume resources every second
RS.Update = function(self)
	--make sure valid
	if (!self.Node) then return end

	--make sure devices are within distance, or break their connections
	beams.CheckDistance(self)

	--get the main node and setup the totals table
	local node = self:Node()
	local stored = self:NodeStoredAmount()
	local storage = self:NodeStorageAmount()
	local requires = self:GetRequirements()

	if (!self._rs.nolife && self:IsActive() && RS.GetHealth(self) <= 0) then self:TurnOff() end --if its dead turn off!

	if (self:IsActive()) then
		--check requirements the entity may have
		for name, value in pairs( requires or {} ) do
			--if device is turned on, and has requirements, and requirements are not meet, turn off
			if (-value > (stored[name] or 0)) then
				self:TurnOff(nil, true)
				break
			end
		end
	end



	local res = RS.GetTable(self, self._rs.resources)

	self:CommitResources( requires )
	self:CommitResources( RS.GetTable(self, self._rs.resources) )

	--set name and desc
	local title = self._rs.name -- .. " " .. self:EntIndex()

	--show on/off status if needed
	if (self._rs.status) then
		if self:IsActive() then
			title = title .. " (On)\n"

			self:WireUpdate( "Active", 1 )
		else
			title = title .. " (Off)\n"

			self:WireUpdate( "Active", 0 )
		end
	else
		title = title .. "\n"
	end

	title = title .. self._rs.desc



	--show current life
	if (!self._rs.nolife) then title = title .. "\n\nVitality: " .. RS.GetHealth(self) .. "%" end

	--show storage value(s)
	if (self._rs.stored && table.Count(RS.GetTable(self, self._rs.stored)) >= 1) then
		title = title .. "\n\nStored:"

		--resources
		for name, value in pairs( RS.GetTable(self, self._rs.stored) or {} ) do
			self:WireUpdate( "Stored: " .. name, math.ceil(stored[name] or 0) )

			if (storage[name]) then
				title = title .. "\n" .. name .. ":-" .. stored[name] .. "/" .. math.ceil(storage[name] or 0)
			elseif stored[name] then
				title = title .. "\n" .. name .. ": " .. stored[name]
			end
		end
	end

	--show the device resources it consumes or generates
	if (self._rs.resources) then
		title = title .. "\n\nResources:"

		--produce and/or consume values
		for name, value in pairs( RS.GetTable(self, self._rs.resources) or {} ) do
			title = title .. "\n\t" .. name .. ": " .. RS.GetNumber(self, value)

			self:WireUpdate( "Resources: " .. name, math.ceil(RS.GetNumber(self, value) or 0) )
		end
	end

	--so what the device requires
	if (self._rs.requires && table.Count(RS.GetTable(self, self._rs.requires)) >= 1) then
		title = title .. "\n\nRequires:"

		--produce and/or consume value`s
		for name, value in pairs( RS.GetTable(self, self._rs.requires) or {} ) do
			title = title .. "\n\t" .. name .. ": " .. RS.GetNumber(self, value)

			self:WireUpdate( "Requires: " .. name, math.ceil(RS.GetNumber(self, value) or 0) )
		end
	end

	--Environment
	if (self.Environment) then
		title = title .. "\n\nEnvironment:"

		if (RS.GetTable(self, self.Environment).radius) then title = title .. "\n\tRadius: " .. RS.GetNumber(self, RS.GetTable(self, self.Environment).radius) end
	end

	--if (self.Owner && self.Owner:IsPlayer()) then title = title .. "\nOwner: " .. self.Owner():Nick() end

	--add extra title if there is one
	if (self._rs.titleextra) then title = title .. self._rs.titleextra end

	--save new title... useful for interactive entities
	self._rs.title = title
end


--function to check if a user is looking at a RS entity and sends them the title of it to prevent to much data being sent
RS.ShowInfo = function()
	for _, ply in pairs(player.GetAll()) do
		local wep = ply:GetActiveWeapon()
		local trace = ply:GetEyeTrace()

		if ValidEntity(wep) and ValidEntity(trace.Entity) and trace.Entity._rs then
			--send the title of the entity. Note: data will cache this if needed to prevent sending the title if it hasnt changed
			data.Send( {"RS.SetEntTitle", trace.Entity:EntIndex()}, trace.Entity:EntIndex(), trace.Entity._rs.title )
		end
	end
end
timer.Create("RS.ShowInfo", 0.5, 0, RS.ShowInfo)


--saves, or consumes, resources amoung all connected devices
RS.CommitResources = function( self, resources, justdoit )
	--if no resources then exit
	if (!resources or table.Count(resources) == 0) then return end

	--check to make sure device is active
	if !(justdoit) and (!self:IsActive()) then return end

	--get the main node
	local node = self:Node()

	--MsgN("------------\nRS.CommitResources\n")
	--PrintTable(resources)
	--MsgN("------------")

	for name, value in pairs( resources ) do
		local storageData = {}

		--get interger
		value = RS.GetNumber( self, value )

		--we need to run through the entities to short them
		--we are doing this so the entity with the least amount of storage gets resources first
		for _, ent in pairs( node.entities ) do
			if (ent && ValidEntity(ent) && ent._rs) then
				--add storage table
				ent._rs.stored = ent._rs.stored or {}

				table.insert(storageData, {Ent = ent, Stored = (RS.GetTable(ent, ent._rs.stored)[name] or 0)})
			end
		end

		--small values first (for storing)
		if (value > 0) then
			table.SortByMember(storageData, "Stored", function(a, b) return a < b end)
		--large values first (for consuming)
		else
			table.SortByMember(storageData, "Stored")
		end

		--loop through the node entities
		for _, _table in pairs( storageData ) do
			--get the entity back from the table data
			local ent = _table.Ent



			--add storage table
			if (!ent._rs.stored) then ent._rs.stored = {} end

			--create the storage value if none
			if (type(ent._rs.stored) == "table" && type(ent._rs.stored[name]) == "number") then
				--MsgN("Storage data: ")
				--PrintTable(ent._rs)

				--ent._rs.stored[name] = RS.GetNumber(ent, ent._rs.stored)[name] or 0

				local maxLocalStorage = 0

				--only do if storing
				--if ent._rs.storage && RS.GetTable(ent, ent._rs.storage)[name] then
					maxLocalStorage = (RS.GetNumber(ent, (RS.GetTable(ent, ent._rs.storage)[name]) or 0))
				--end

				--MsgN("---------------------------\nmaxLocalStorage:", maxLocalStorage, "\n---------------------------\n")

				if (value > 0) then
					--get how much we can store
					--local canStore = math.Clamp(math.Clamp(maxLocalStorage - RS.GetTable(ent, ent._rs.stored)[name], 0, value), 0, maxLocalStorage)

					local canStore = math.Clamp(math.Clamp(maxLocalStorage - RS.GetTable(ent, ent._rs.stored)[name], 0, value), 0, maxLocalStorage)

					--store the new value
					ent._rs.stored[name] = (RS.GetTable(ent, ent._rs.stored)[name] + canStore)

					--subtract the value from consuming on next entity
					value = math.Clamp((value - canStore), 0, 99999999999999999999)

					--steal some resources from LS
					RS.LSSteal(ent, name, canStore)
				else
					--get how much we can consume
					--local canConsume = math.Clamp(math.Clamp(value, -RS.GetTable(ent, ent._rs.stored)[name], 0), -99999999999999999999, 0)

					local canConsume = math.Clamp(math.Clamp(value, -RS.GetTable(ent, ent._rs.stored)[name], 0), -maxLocalStorage, 0)

					--consume the value
					ent._rs.stored[name] = RS.GetTable(ent, ent._rs.stored)[name] + canConsume

					--subtract the value from consuming on next entity
					value = math.Clamp(value - canConsume, -99999999999999999999, 0)

					--give back some resources to LS
					RS.LSGive(ent, name, -canConsume)
				end
			end

			--nothing left to store then exit
			if (value == 0) then break end
		end
	end
end

--checks to see if two entities can be linked. does not check if already in system though.
RS.CanLink = function( self, ent )
	return (self._rs && ent._rs && !ent._rs.canlink)
end

--if a value is a function, execute it and return it, else, return the value back
RS.GetNumber = function( ent, value, addition )
	--convert function into a value
	if (type(value) == "function") then value = math.ceil(value(ent)) end
	return math.ceil((addition or 0) + (value or 0))
end

--returns a interger from interger or function
RS.GetTable = function( ent, tbl )
	--convert function into a value
	if (type(tbl) == "function") then return tbl(ent) end
	return tbl || {}
end

--build the DupeInfo table and save it as an entity mod
RS.BuildDupeInfo = function( self )
	--dont apply dup info if not registered with RS
	if !self._rs then return end

	--store RS info
	duplicator.StoreEntityModifier( self, "RSDupeInfo",  {
		name = self._rs.name,
		model = self._rs.model,
		header = self._rs.header,
		tool = self._rs.tool,
		category = self._rs.category,
		stored = RS.GetTable(ent, self._rs.stored)
	})
end

--apply the DupeInfo
RS.ApplyDupeInfo = function( ply, self, info, GetEntByID )
	--get the device settings (also prevents cheating)
	local deviceSettings = table.Copy(RS.Tools[info.header][info.name][info.category][info.name .. info.model])

	--dont spawn a device if it doesnt exist
	if (!deviceSettings) then return end

	--add to resource system
	RS.Setup( self, deviceSettings )

	--update stored amount
	self._rs.stored = info.stored

	--apply custom settings if any
	if (deviceSettings.createfunction) then deviceSettings.createfunction( self ) end
end

--round to decimal point
function RS.round(num, idp)
	local mult = 10^(idp or 0)
	return math.floor(num * mult + 0.5) / mult
end